package org.swift.commons.client;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.ProtocolException;
import java.net.URL;
import java.net.URLEncoder;
import java.security.cert.X509Certificate;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSession;

import org.apache.axiom.om.OMAbstractFactory;
import org.apache.axiom.om.OMElement;
import org.apache.axiom.om.OMFactory;
import org.apache.axis2.AxisFault;

import org.apache.log4j.Logger;
import org.swift.commons.util.StringUtils;

/**
 * @author jroberts
 */
@SuppressWarnings("unchecked")
public class RestCall implements Call {
	private final static Logger log = Logger.getLogger(RestCall.class);

	private OMFactory factory = OMAbstractFactory.getOMFactory();

	private final static String ENCODING = "UTF-8";
	private final static int DEFAULT_TIMEOUT = 120000;
	private final static String DEFAULT_METHOD = "GET";
	private final static String DEFAULT_CONTENT_TYPE = "application/xml";
	private final static String FORM_CONTENT_TYPE = "application/x-www-form-urlencoded";
	private final static String AUTH_HEADER_NAME = "Authorization";
	private final static String COOKIE_HEADER_NAME = "Cookie";

	public final static String POST_DATA = "{postData}";

	private String url;

	private String httpMethod;

	private String contentType;

	private int timeoutMS;

	private Map<String, String> headers = new HashMap<String, String>();

	private String returnElement = "return";
	
	private String username;
	
	private String password;

	public RestCall(String url, String httpMethod) throws MalformedURLException {
		this(url, httpMethod, null, DEFAULT_TIMEOUT);
	}

	public RestCall(String url, String httpMethod, int timeoutMs)
			throws MalformedURLException {
		this(url, httpMethod, null, timeoutMs);
	}

	public RestCall(String url, String httpMethod, String contentType, String username, String password)
			throws MalformedURLException {
		this(url, httpMethod, contentType, DEFAULT_TIMEOUT, username, password);
	}

	public RestCall(String url, String httpMethod, String contentType)
			throws MalformedURLException {
		this(url, httpMethod, contentType, DEFAULT_TIMEOUT);
	}

	public RestCall(String url, String httpMethod, String contentType, int timeoutMs)
			throws MalformedURLException {
		this(url, httpMethod, contentType, timeoutMs, null, null);
	}

	public RestCall(String url, String httpMethod, String contentType, int timeoutMs, String username, String password)
			throws MalformedURLException {
		this.url = url;
		this.httpMethod = (StringUtils.isEmpty(httpMethod) ?
				DEFAULT_METHOD : httpMethod);
		this.contentType = (StringUtils.isEmpty(contentType) ?
				DEFAULT_CONTENT_TYPE : contentType);
		this.timeoutMS = timeoutMs;
		this.username = username;
		this.password = password;
	}
	
	

	public void setCustomHeaders(Map<String, String> headersMap) {
		this.headers.putAll(headersMap);
	}
	
	protected HttpInputStream send(Map<String, ?> parameterMap, Map<String, ?> restParameterMap, boolean returnOutput) throws AxisFault {
		try {
			String url = this.url;
			String restParams = this.encodeRestParameters(restParameterMap);
			HttpURLConnection conn;
			if (StringUtils.isNotEmpty(restParams)) {
				if ( url.endsWith(":") ) {
					url += "//";
				}
				else if( !url.isEmpty() && !url.endsWith("/") ){
					url += "/";
				}

				url += restParams;
			}
			// If sending {postData}, set content type
			if(parameterMap.get(POST_DATA)!=null){
				conn = this.createConnection(new URL(url), true);
				conn.setRequestProperty("CONTENT-TYPE", this.contentType);
				Object o = parameterMap.get(POST_DATA);
				if(o!=null && o instanceof String){
					if(log.isDebugEnabled()) {
						log.debug("Post data for url " + url + " is: " + o);
					}
					OutputStream os = conn.getOutputStream();
					os.write(((String)o).getBytes());
					os.close();
				}
			// else standard parameters
			}else{
				String parameters = this.encodeParameters(parameterMap);
				// attach parameter string to url
				if (isURLEncodedParams() && StringUtils.isNotEmpty(parameters)) {
					url += "?" + parameters;
					conn = this.createConnection(new URL(url), true);
				// write parameter string to response body
				}else if(StringUtils.isNotEmpty(parameters)){
					conn = this.createConnection(new URL(url), true);
					conn.setRequestProperty("CONTENT-TYPE", FORM_CONTENT_TYPE);
					OutputStream os = conn.getOutputStream();
					os.write(parameters.getBytes());
					os.close();
				// No parameters exist, use url as is
				}else{
					conn = this.createConnection(new URL(url), true);
				}
			}
			// there is no point in returning an input stream if
			// we know the request wasn't successful
			final int responseCode = conn.getResponseCode();
			if(responseCode < 200 || responseCode > 299) {
				// FIXME: handle 1xx responses?
				throw new BadHttpResponseFault(conn);
			}
			else {
				return processResult(conn);
			}
		} catch (ProtocolException e) {
			throw AxisFault.makeFault(e);
		} catch (IOException e) {
			throw AxisFault.makeFault(e);
		}
	}

	public MeasuredInputStream sendRecieve(Map<String, ?> parameterMap) throws AxisFault {
		return this.sendRecieve(parameterMap, Collections.EMPTY_MAP);
	};

	public MeasuredInputStream sendRecieve(Map<String, ?> parameterMap, Map<String, ?> restParameterMap) throws AxisFault {
		return send(parameterMap, restParameterMap, true);
	};

	protected HttpInputStream processResult(HttpURLConnection conn) throws IOException {		
		return new HttpInputStream(conn, conn.getInputStream());		
	}

	protected HttpURLConnection createConnection(URL url, boolean doOutput) throws IOException {
		if (log.isDebugEnabled()) {
			log.debug("opening connection to : " + url);
		}
		
		HttpURLConnection conn = (HttpURLConnection) url.openConnection();
		
		for ( Map.Entry<String,String> headerEntry : this.headers.entrySet() ) {
			conn.setRequestProperty(headerEntry.getKey(), headerEntry.getValue());
		}

		String cookieString = CookieUtil.generateCookieString();
		if(StringUtils.isNotBlank(cookieString)){
			conn.setRequestProperty(COOKIE_HEADER_NAME, cookieString);
		}
		String basicAuth = getAuthCredentials();
		if(StringUtils.isNotBlank(basicAuth)){
			conn.setRequestProperty(AUTH_HEADER_NAME, basicAuth);
		}else{
			basicAuth = RequestUtils.getRequestHeader(AUTH_HEADER_NAME);
			if(StringUtils.isNotBlank(basicAuth)){
				conn.setRequestProperty(AUTH_HEADER_NAME, basicAuth);
			}
		}
		if (conn instanceof HttpsURLConnection) {
			String verifyHostName = System.getProperty("org.swift.commons.client.RestCall.verifyHostName");
			if ("true".equals(verifyHostName))
			((HttpsURLConnection)conn).setHostnameVerifier(new HostnameVerifier() {
	
				public boolean verify(String arg0, SSLSession arg1) {
					return true;
				}
				
			});
		}
		conn.setRequestMethod(this.httpMethod);
		conn.setDoOutput(doOutput);
		conn.setReadTimeout(this.timeoutMS);
		return conn;
	}

	/**
	 * endcodes the parameter map to a string
	 * 
	 * @param parameters
	 * @return
	 * @throws UnsupportedEncodingException
	 */
	protected String encodeParameters(Map<String, ?> parametersMap)
			throws UnsupportedEncodingException {
		Class<?> type = null;
		StringBuffer buffer = new StringBuffer();
		for (Entry<String, ?> entry : parametersMap.entrySet()) {
			Object o = entry.getValue();
			type = o.getClass();
			String value = "";
			if (type.isArray()){
				Object[] values = (Object[]) o;
				if (values.length == 0) {
					continue;
				} else {
					for (Object val : values){
						value = val.toString();
						addParamToBuffer (buffer,entry.getKey(),value);
					}
				}
			} else {
				value = o.toString();
				addParamToBuffer (buffer,entry.getKey(),value);
			}
		}
		
		String parameters = buffer.toString();
		if (parameters.length() > 0) {
			parameters = parameters.replaceAll("\\+", "%20");				
		}
		
		return parameters;
	}

	/**
	 * endcodes the parameter map to a string for restful calls
	 * 
	 * @param parameters
	 * @return
	 * @throws UnsupportedEncodingException
	 */
	protected String encodeRestParameters(Map<String, ?> parametersMap)
			throws UnsupportedEncodingException {
		Class<?> type = null;
		StringBuffer buffer = new StringBuffer();
		for (Entry<String, ?> entry : parametersMap.entrySet()) {
			Object o = entry.getValue();
			type = o.getClass();
			String value = "";
			if (type.isArray()){
				Object[] values = (Object[]) o;
				if (values.length == 0) {
					continue;
				} else {
					for (Object val : values){
						value = val.toString();
						addRestParamToBuffer(buffer,value);
					}
				}
			} else {
				value = o.toString();
				addRestParamToBuffer(buffer,value);
			}
		}
		
		String parameters = buffer.toString();
		if (parameters.length() > 0) {
			parameters = parameters.replaceAll("\\+", "%20");				
		}
		
		return parameters;
	}
	
	private void addParamToBuffer (final StringBuffer currentBuffer, 
			final String paramName, final String value) throws UnsupportedEncodingException{
		if (currentBuffer.length() > 0)
			currentBuffer.append('&');
		currentBuffer.append(URLEncoder.encode(paramName, ENCODING) + "=" + 
			URLEncoder.encode(value, ENCODING));
	}
	
	private void addRestParamToBuffer (final StringBuffer currentBuffer, 
			final String value) throws UnsupportedEncodingException{
		if (currentBuffer.length() > 0)
			currentBuffer.append('/');
		currentBuffer.append(value);
	}
	
	private boolean isURLEncodedParams() {
		return ("GET".equals(httpMethod) 
				|| "DELETE".equals(httpMethod)
				|| "HEAD".equals(httpMethod));		
	}
	
	private String getAuthCredentials(){
		if(StringUtils.isNotBlank(this.username)){
			return RequestUtils.createBasicAuthCredentials(this.username, this.password);
		}
		return null;
	}

	public InputStream sendRecieve(OMElement element) throws AxisFault {
		//TODO create a map from the xml
		throw new RuntimeException("this method is not supported.");
	}

	public void sendRobust(OMElement element) throws AxisFault {
		//TODO create a map from the xml
		throw new RuntimeException("this method is not supported."); 
	}

	public void sendRobust(Map<String, ?> parametersMap) throws AxisFault {
		this.sendRobust(parametersMap, Collections.EMPTY_MAP);
	}

	public void sendRobust(Map<String, ?> parametersMap, Map<String, ?> restParametersMap) throws AxisFault {
		this.send(parametersMap, restParametersMap, false);
	}

	public String getURL() {
		return this.url.toString();
	}

	protected String getHttpMethod() {
		return this.httpMethod;
	}

	protected String getContentType() {
		return this.contentType;
	}

	public String getReturnElement() {
		return returnElement;
	}

	public void setReturnElement(String returnElement) {
		this.returnElement = returnElement;
	}

	public OMFactory getFactory() {
		return factory;
	}

	public void setFactory(OMFactory factory) {
		this.factory = factory;
	}

}
